package cz.muni.fi.pv168.confusinglibrary.libs;

import cz.muni.fi.pv168.confusinglibrary.database.DBUtils;
import cz.muni.fi.pv168.confusinglibrary.database.ServiceFailureException;
import java.util.List;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;

/**
 * Manages database of authors.
 *
 * @author Riema
 */
public class AuthorManagerDB implements AuthorManager {

    private DataSource ds;
    private static final Logger logger = Logger.getLogger(
            AuthorManagerDB.class.getName());

    public AuthorManagerDB() {
    }

    public void setDataSource(DataSource ds) {
        this.ds = ds;
    }

    private void checkDataSource() {
        if (ds == null) {
            throw new IllegalStateException("DataSource is not set");
        }
    }

    /**
     * This adds Author to database of already existing Authors.
     * It's expected that author isn't allready in database - ie. id is  null.
     *
     * @param author Author to be added, cannot be null, author's id is supposed to be null
     * @return Author after adding, ie. with proper id
     * @throws NullPointerException if parametr is null
     * @throws IllegalArgumentException if author has id (!= null)
     */
    public Author addAuthor(Author author) {
        if (author == null) {
            throw new NullPointerException("author");
        }
        if (author.getId() != null) {
            throw new IllegalArgumentException("Author is already in list.");
        }

        Connection conn = null;
        try {
            Logger.getLogger(AuthorManagerDB.class.getName()).log(Level.FINER, "Adding author");
            conn = ds.getConnection();
            PreparedStatement st = conn.prepareStatement("INSERT INTO app.author (name,surname,born,died) VALUES (?,?,?,?)",
                    Statement.RETURN_GENERATED_KEYS);
            st.setString(1, author.getName());
            st.setString(2, author.getSurname());
            st.setDate(3, (author.getBorn() == null ? null : new Date(author.getBorn().getTime())));
            st.setDate(4, (author.getDied() == null ? null : new Date(author.getDied().getTime())));

            if (st.executeUpdate() != 1){
                logger.log(Level.SEVERE, "Error when inserting author into DB. Database was not able to execute command for adding author");
                throw new ServiceFailureException("Database was not able to execute command for adding author");
            }

            Long id = DBUtils.getId(st.getGeneratedKeys());
            author.setId(id);
        } catch (SQLException ex) {
            logger.log(Level.SEVERE, "Error when inserting author into DB", ex);
            throw new RuntimeException("Error when inserting into DB", ex);
        } finally {
            DBUtils.closeQuietly(conn);
        }
        Logger.getLogger(AuthorManagerDB.class.getName()).log(Level.FINER, "Author added");
        return author;
    }

    /**
     * This removes Author from database.
     * And sets null as id of removed Author.
     *
     * @param author Author to be removed, cannot be null
     * @throws NullPointerException if paramter is null
     * @throws IllegalArgumentException if author isn't in database or has null id
     */
    public void removeAuthor(Author author) {
        if (author == null) {
            throw new NullPointerException("author");
        }
        if (author.getId() == null) {
            throw new IllegalArgumentException("Author's id is null");
        }

        Connection conn = null;
        try {
            Logger.getLogger(AuthorManagerDB.class.getName()).log(Level.FINER, "Removing author");
            conn = ds.getConnection();
            PreparedStatement st = conn.prepareStatement("DELETE FROM app.author WHERE id=?");
            st.setLong(1, author.getId());

            if (st.executeUpdate() == 0) {
                throw new IllegalArgumentException("Author doesn't exist");
            }

            author.setId(null);
            Logger.getLogger(AuthorManagerDB.class.getName()).log(Level.FINER, "Author removed");
        } catch (SQLException ex) {
            logger.log(Level.SEVERE, "Error when deleting author from DB", ex);
            throw new RuntimeException("Error when deleting author from DB", ex);
        } finally {
            DBUtils.closeQuietly(conn);
        }
    }

    /**
     * This updates information about Author.
     * Author is supposed to in database,
     * attributes can be changed all except id
     *
     * @param author Author with fixed id, and with updated information, cannot be null
     * @return  Author after updating
     * @throws NullPointerException if parametr is null
     * @throws IllegalArgumentException if author with this id isn't in database or id is null
     */
    public void updateAuthor(Author author) {
        if (author == null) {
            throw new NullPointerException("author");
        }
        if (author.getId() == null) {
            throw new IllegalArgumentException("Author's id is null");
        }

        Connection conn = null;
        try {
            Logger.getLogger(AuthorManagerDB.class.getName()).log(Level.FINER, "Updating author");
            conn = ds.getConnection();
            PreparedStatement st = conn.prepareStatement("UPDATE app.author SET name = ?, surname = ?, born = ?, died = ? WHERE id = ?");
            st.setString(1, author.getName());
            st.setString(2, author.getSurname());
            st.setDate(3, (author.getBorn() == null ? null : new Date(author.getBorn().getTime())));
            st.setDate(4, (author.getDied() == null ? null : new Date(author.getDied().getTime())));
            st.setLong(5, author.getId());

            if (st.executeUpdate() < 1) {
                throw new IllegalArgumentException("Author doesn't exist");
            }

            Logger.getLogger(AuthorManagerDB.class.getName()).log(Level.FINER, "Author removed");
        } catch (SQLException ex) {
            logger.log(Level.SEVERE, "Error when updating author", ex);
            throw new RuntimeException("Error when updating author", ex);
        } finally {
            DBUtils.closeQuietly(conn);
        }
    }

    /**
     *
     * @return List of all Authors, if there is no Author returns empty List
     */
    public List<Author> getAllAuthors() {
        Connection conn = null;
        try {
            conn = ds.getConnection();
            PreparedStatement st = conn.prepareStatement("SELECT * from app.author");

            ResultSet rs = st.executeQuery();

            List<Author> found = new ArrayList<Author>();

            while (rs.next()) {
                Author result = new Author();
                result.setId(rs.getLong("id"));
                result.setName(rs.getString("name"));
                result.setSurname(rs.getString("surname"));
                result.setBorn(rs.getDate("born") == null ? null : new java.util.Date(rs.getDate("born").getTime()));
                result.setDied(rs.getDate("died") == null ? null : new java.util.Date(rs.getDate("died").getTime()));
                found.add(result);
            }
            return Collections.unmodifiableList(found);
        } catch (SQLException ex) {
            logger.log(Level.SEVERE, "Error when getting all authors from DB", ex);
            throw new RuntimeException("Error when getting all authors from DB", ex);
        } finally {
            DBUtils.closeQuietly(conn);
        }
    }

    /**
     *
     * @param id we're looking for author having such id
     * @return Author with this id or null if there isn't such author
     * @throws NullPointerException if parametr is null
     */
    public Author findAuthorById(Long id) {
        if (id == null) {
            throw new NullPointerException("id");
        }

        Connection conn = null;
        try {
            conn = ds.getConnection();
            PreparedStatement st = conn.prepareStatement(
                    "SELECT id, name, surname, born, died FROM app.author WHERE id = ?");
            st.setLong(1, id);

            ResultSet rs = st.executeQuery();

            if (rs.next()) {
                Author result = new Author();
                result.setId(rs.getLong("id"));
                result.setName(rs.getString("name"));
                result.setSurname(rs.getString("surname"));
                result.setBorn(rs.getDate("born") == null ? null : new java.util.Date(rs.getDate("born").getTime()));
                result.setDied(rs.getDate("died") == null ? null : new java.util.Date(rs.getDate("died").getTime()));
                assert !rs.next();
                return result;
            } else {
                return null;
            }

        } catch (SQLException ex) {
            logger.log(Level.SEVERE, "Error when getting author from DB in findAuthorById", ex);
            throw new RuntimeException("Error when getting author from DB in findAuthorById", ex);
        } finally {
            DBUtils.closeQuietly(conn);
        }
    }

    /**
     * All Authors with given surname
     *
     * @param surname parametr for searching - cannot be null
     * @return List of all Authors with this surname (can be empty)
     * @throws NullPointerException if parametr is null
     */
    public List<Author> findAuthorsByName(String surname) {
        return findAuthorsByName(surname, null);
    }

    /**
     * All Authors with given surname and name
     *
     * @param surname parametr for searching - cannot be null
     * @param name option parametr for searching
     * @return List of all Authors with this surname and name (can be empty)
     * @throws NullPointerException if first parametr (surname) is null
     */
    public List<Author> findAuthorsByName(String surname, String name) {
        if (surname == null) {
            throw new NullPointerException("surname");
        }

        Connection conn = null;
        try {
            conn = ds.getConnection();
            PreparedStatement st = null;
            if (name != null) {
                st = conn.prepareStatement(
                        "SELECT id, name, surname, born, died FROM app.author WHERE surname = ? AND name = ?");
                st.setString(1, surname);
                st.setString(2, name);
            } else {
                st = conn.prepareStatement(
                        "SELECT id, name, surname, born, died FROM app.author WHERE surname = ?");
                st.setString(1, surname);
            }

            ResultSet rs = st.executeQuery();

            List<Author> authors = new ArrayList<Author>();

            while (rs.next()) {
                Author result = new Author();
                result.setId(rs.getLong("id"));
                result.setName(rs.getString("name"));
                result.setSurname(rs.getString("surname"));
                result.setBorn(rs.getDate("born") == null ? null : new java.util.Date(rs.getDate("born").getTime()));
                result.setDied(rs.getDate("died") == null ? null : new java.util.Date(rs.getDate("died").getTime()));
                authors.add(result);
            }

            return authors;

        } catch (SQLException ex) {
            logger.log(Level.SEVERE, "Error when getting authors from DB in findAuthorsByName", ex);
            throw new RuntimeException("Error when getting authors from DB in findAuthorsByName", ex);
        } finally {
            DBUtils.closeQuietly(conn);
        }

    }
}
